/*
 * Copyright (c) 2024 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { InputStream } from './internal/InputStream'
import { DfuBaseService } from './DfuBaseService'
import { DfuProgressInfo } from './DfuProgressInfo'
import { DfuService } from './DfuService'
import { DfuServiceInitiator } from './DfuServiceInitiator'
import { BootloaderScannerFactory } from './internal/scanner/BootloaderScannerFactory'
import { ArchiveInputStream } from './internal/ArchiveInputStream'
import { BusinessError } from '@ohos.base'

import fs from '@ohos.file.fs';
import ble from '@ohos.bluetooth.ble';
import hilog from '@ohos.hilog';

export abstract class BaseDfuImpl implements DfuService {
  public TAG: string = "DfuImpl";
  public DOMAIN: number = 0x8632;

  static GENERIC_ATTRIBUTE_SERVICE_UUID: string = '0x00001801-0000-1000-8000-00805F9B34FB';
  static SERVICE_CHANGED_UUID: string = '00002A05-0000-1000-8000-00805F9B34FB';
  static CLIENT_CHARACTERISTIC_CONFIG: string = '00002902-0000-1000-8000-00805f9b34fb';
  static NOTIFICATIONS: number = 1;
  static INDICATIONS: number = 2;

  private static HEX_ARRAY: string = "0123456789ABCDEF";
  private static MAX_PACKET_SIZE_DEFAULT: number = 20; // the default maximum number of bytes in one packet is 20.

  mFirmwareStream: InputStream;
  mInitPacketStream: InputStream;

  /**
   * The target GATT device.
   */
  mGatt: ble.GattClientDevice;
  /**
   * The firmware type. See TYPE_* constants.
   */
  mFileType: number;
  /**
   * Flag set to true if sending was paused.
   */
  mPaused: boolean;
  /**
   * Flag set to true if sending was aborted.
   */
  mAborted: boolean;
  /**
   * Flag indicating whether the device is still connected.
   */
  mConnected: boolean;
  /**
   * Flag indicating whether the request was completed or not
   */
  mRequestCompleted: boolean;
  /**
   * Flag sent when a request has been sent that will cause the DFU target to reset.
   * Often, after sending such command, Android throws a connection state error.
   * If this flag is set the error will be ignored.
   */
  mResetRequestSent: boolean;
  /**
   * The number of the last error that has occurred or 0 if there was no error.
   */
  mError: number;
  /**
   * Latest data received from device using notification.
   */
  mReceivedData: Uint8Array  = null;
  mBuffer: Uint8Array = new Uint8Array(BaseDfuImpl.MAX_PACKET_SIZE_DEFAULT);
  mService: DfuBaseService;
  mProgressInfo: DfuProgressInfo;
  mImageSizeInBytes: number;
  mInitPacketSizeInBytes: number;
  private mCurrentMtu: number;

  constructor(service: DfuBaseService) {
    hilog.info(this.DOMAIN, this.TAG, 'BaseDfuImpl constructor');
    this.mService = service;
    this.mProgressInfo = service.mProgressInfo;
    this.mConnected = true;
  }

  async isClientCompatible(gattServiceList: Array<ble.GattService>): Promise<boolean> {
    hilog.error(this.DOMAIN, this.TAG, 'BaseDfuImpl isClientCompatible')
    throw new Error('Method not implemented.');
  }

  performDfu(): void {
    throw new Error('Method not implemented.');
  }

  release(): void {
    throw new Error('Method not implemented.');
  }

  public pause() {
    this.mPaused = true;
  }

  public resume() {
    this.mPaused = false;
    // notifyLock();
  }

  public abort() {
    this.mPaused = false;
    this.mAborted = true;
    // notifyLock();
  }

  public onBondStateChanged(state: number) {
    this.mRequestCompleted = true;
    // notifyLock();
  }

  getGattCallback() {
    return;
  }

  public isBonded(): boolean {
    let remoteDevList: string[] = ble.getConnectedBLEDevices()
    if (remoteDevList.length > 0 && this.mConnected) {
      return true;
    }
    return false;
  }

  public abstract shouldScanForBootloader(): boolean;

  public abstract finalize(forceRefresh: boolean, scanForBootloader: boolean);

  public initialize(gatt: ble.GattClientDevice, fileType: number,
    firmwareStream: InputStream, initPacketStream: InputStream): boolean {
    hilog.info(this.DOMAIN, this.TAG, 'BaseDfuImpl initialize.');
    this.mGatt = gatt;
    this.mFileType = fileType;
    this.mFirmwareStream = firmwareStream;
    this.mInitPacketStream = initPacketStream;

    //TODO: get currentPart, totalParts, mCurrentMtu ...
    let currentPart: number = 1;
    let totalParts: number = 1;
    this.mCurrentMtu = 23;

    // Sending App together with SD or BL is not supported. It must be spilt into two parts.
    if (fileType > DfuBaseService.TYPE_APPLICATION) {
      hilog.warn(this.DOMAIN, this.TAG, "DFU target does not support (SD/BL)+App update, splitting into 2 parts");
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Sending system components");
      this.mFileType &= ~DfuBaseService.TYPE_APPLICATION; // clear application bit
      totalParts = 2;

      // Set new content type in the ZIP Input Stream and update sizes of images
      let zhis: ArchiveInputStream = (this.mFirmwareStream as ArchiveInputStream);
      zhis.setContentType(this.mFileType);
    }

    if (currentPart == 2) {
      this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_WARNING, "Sending application");
    }

    let size: number = 0;
    try {
      if (initPacketStream != null) {
        if (initPacketStream.markSupported()) {
          initPacketStream.reset();
        }
        size = initPacketStream.available();
      }
    } catch (e) {
      // ignore
      hilog.error(this.DOMAIN, this.TAG, `initPacketStream error: ${JSON.stringify(e)}`);
    }
    this.mInitPacketSizeInBytes = size;
    try {
      if (firmwareStream.markSupported()) {
        if (firmwareStream instanceof ArchiveInputStream) {
          (firmwareStream as ArchiveInputStream).fullReset();
        } else {
          firmwareStream.reset();
        }
      }
      size = firmwareStream.available();
    } catch (e) {
      size = 0;
      // not possible
      hilog.error(this.DOMAIN, this.TAG, `firmwareStream error: ${JSON.stringify(e)}`);
    }
    this.mImageSizeInBytes = size;
    this.mProgressInfo.init(size, currentPart, totalParts);

    //TODO: if device Bond, to following
    // if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
    //   final BluetoothGattService genericAttributeService = gatt.getService(GENERIC_ATTRIBUTE_SERVICE_UUID);
    //   if (genericAttributeService != null) {
    //     final BluetoothGattCharacteristic serviceChangedCharacteristic = genericAttributeService.getCharacteristic(SERVICE_CHANGED_UUID);
    //     if (serviceChangedCharacteristic != null) {
    //       // Let's read the current value of the Service Changed CCCD
    //       final boolean serviceChangedIndicationsEnabled = isServiceChangedCCCDEnabled();
    //
    //       if (!serviceChangedIndicationsEnabled)
    //         enableCCCD(serviceChangedCharacteristic, INDICATIONS);
    //
    //       logi("Service Changed indications enabled");
    //       mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_APPLICATION, "Service Changed indications enabled");
    //     }
    //   }
    // }
    return true;
  }

  public async enableCCCD(characteristic: ble.BLECharacteristic, type: number) {
    hilog.info(this.DOMAIN, this.TAG, `enableCCCD ${JSON.stringify(characteristic)}, ${type.toString()}`);
    let gatt: ble.GattClientDevice = this.mGatt;
    let debugString = type === BaseDfuImpl.NOTIFICATIONS ? "notifications" : "indications";
    if (!this.mConnected) {
      hilog.error(this.DOMAIN, this.TAG, `Unable to set ${debugString} state: device disconnected`);
      throw new Error(`Unable to set ${debugString} state: device disconnected`);
    }
    if (this.mAborted) {
      throw new Error(`UploadAbortedException`);
    }

    this.mRequestCompleted = false;
    this.mReceivedData = null;
    this.mError = 0;

    hilog.info(this.DOMAIN, this.TAG, `Enabling ${debugString} ...`);
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE,
      "Enabling " + debugString + " for " + characteristic.characteristicUuid);

    // enable notifications locally
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG,
      "gatt.setCharacteristicNotification(" + characteristic.characteristicUuid + ", true)");
    await gatt.setCharacteristicChangeNotification(characteristic, true);
    hilog.info(this.DOMAIN, this.TAG, 'setCharacteristicChangeNotification true');

    // enable notifications on the device
    this.mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_DEBUG,
      "gatt.writeDescriptor(" + characteristic.descriptors + (type == BaseDfuImpl.NOTIFICATIONS ?
      ", value=0x01-00)" : ", value=0x02-00)"));
    let descriptorItem: ble.BLEDescriptor;
    for (let descipter of characteristic.descriptors) {
      if (descipter.descriptorUuid === BaseDfuImpl.CLIENT_CHARACTERISTIC_CONFIG.toUpperCase()) {
        descriptorItem = descipter;
      }
    }
    hilog.info(this.DOMAIN, this.TAG,
      `gatt.writeDescriptor(${descriptorItem.descriptorUuid}, (type == NOTIFICATIONS?value=0x01-00):value=0x02-00)"))`);

    let buf: ArrayBuffer = new ArrayBuffer(2);
    let ubuf: Uint8Array = new Uint8Array(buf);
    if (type === BaseDfuImpl.NOTIFICATIONS) {
      ubuf[0] = 0x1;
      ubuf[1] = 0x0;
    } else {
      ubuf[0] = 0x2;
      ubuf[1] = 0x0;
    }
    descriptorItem.descriptorValue = buf;
    await gatt.writeDescriptorValue(descriptorItem);
    hilog.info(this.DOMAIN, this.TAG, `gatt.writeDescriptorValue : ${JSON.stringify(descriptorItem)}`);
  }

  public async writeOpCode(characteristic: ble.BLECharacteristic, value: Uint8Array, reset: boolean) {
    if (this.mAborted) {
      hilog.error(this.DOMAIN, this.TAG, 'throw new UploadAbortedException');
      this.mProgressInfo.setProgress(DfuBaseService.PROGRESS_ABORTED);
      this.mService.terminateConnection(this.mGatt, DfuBaseService.ERROR_DEVICE_DISCONNECTED);
      throw new Error('UploadAbortedException');
    }
    this.mReceivedData = null;
    this.mError = 0;
    this.mRequestCompleted = false;
    this.mResetRequestSent = reset;

    // hilog.info(this.DOMAIN, this.TAG,
    //   `writeOpCode Writing to characteristic ${JSON.stringify(characteristic)} value: ${JSON.stringify(value)}`);
    characteristic.characteristicValue = value.buffer;
    await this.mGatt.writeCharacteristicValue(characteristic, ble.GattWriteType.WRITE);
    hilog.info(this.DOMAIN, this.TAG, `writeOpCode Writing to characteristic: ${JSON.stringify(characteristic)}`);

  }

  public restartService(scanForBootloader: boolean, serviceUuid: string) {
    let newAddress: string;
    if (scanForBootloader) {
      // final long delay = intent.getLongExtra(DfuBaseService.EXTRA_SCAN_DELAY, 0);
      // final long timeout = intent.getLongExtra(DfuBaseService.EXTRA_SCAN_TIMEOUT, DfuServiceInitiator.DEFAULT_SCAN_TIMEOUT);
      // mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_VERBOSE, "Scanning for the DFU Bootloader... (timeout " + timeout + " ms)");
      let timeout: number = DfuServiceInitiator.DEFAULT_SCAN_TIMEOUT;
      hilog.info(this.DOMAIN, this.TAG, `Scanning for the DFU Bootloader... ${timeout} ms`)

      newAddress = BootloaderScannerFactory
        .getScanner(this.mService.wmsg.deviceAddress, serviceUuid, this.mService.wmsg)
        .searchUsing(this.mService.getDeviceSelector(), timeout);
      hilog.info(this.DOMAIN, this.TAG, "Scanning for new address finished with: " + newAddress);
      if (newAddress != null)
        // mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "DFU Bootloader found with address " + newAddress);
        hilog.info(this.DOMAIN, this.TAG, "DFU Bootloader found with address " + newAddress);
      else {
        // mService.sendLogBroadcast(DfuBaseService.LOG_LEVEL_INFO, "DFU Bootloader not found. Trying the same address...");
        hilog.info(this.DOMAIN, this.TAG, "DFU Bootloader not found. Trying the same address...");
      }
    }

    // TODO: need restart service
    // if (newAddress != null)
    // intent.putExtra(DfuBaseService.EXTRA_DEVICE_ADDRESS, newAddress);
    //
    // // Reset the DFU attempt counter
    // intent.putExtra(DfuBaseService.EXTRA_DFU_ATTEMPT, 0);
    //
    // final boolean foregroundService = intent.getBooleanExtra(DfuBaseService.EXTRA_FOREGROUND_SERVICE, true);
    // if (foregroundService && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
    // mService.startForegroundService(intent);
    // else
    // mService.startService(intent);

  }

  protected abstract getDfuServiceUUID(): string;


}